########################################################################
#  Searx-Qt - Lightweight desktop application for Searx.
#  Copyright (C) 2020-2022  CYBERDEViL
#
#  This file is part of Searx-Qt.
#
#  Searx-Qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-Qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

from PyQt5.QtCore import pyqtSignal, QObject

from searxqt.core.searx import SearX, SearxConfigHandler
from searxqt.core import log

from searxqt.thread import Thread, ThreadManagerProto

from searxqt.translations import _


class SearchStatus:
    Done = 0
    Busy = 1


class SearchBehaviour:
    Normal = 0
    RandomEvery = 1


class CategoryModel(QObject):
    """ stateChanged; emitted when this category gets enabled or disabled.
    str: category key
    bool: state
    """
    stateChanged = pyqtSignal(str, bool)
    """ changed; emitted when a engine is added or removed from this category.
    str: category key
    """
    changed = pyqtSignal(str)

    def __init__(self, key, name, checked=False, parent=None):
        QObject.__init__(self, parent=parent)
        self.__key = key
        self.__name = name
        self.__checked = checked

    @property
    def name(self): return self.__name

    @property
    def key(self): return self.__key

    def isChecked(self):
        return self.__checked

    def check(self):
        self.__checked = True
        self.stateChanged.emit(self.key, True)

    def uncheck(self):
        self.__checked = False
        self.stateChanged.emit(self.key, False)


class UserCategoryModel(CategoryModel):
    def __init__(self, key, name, checked=False, engines=None, parent=None):
        CategoryModel.__init__(self, key, name, checked=checked, parent=parent)

        # https://docs.python.org/3/faq/programming.html?highlight=default%20shared%20values%20objects#why-are-default-values-shared-between-objects
        self.__engines = engines if engines is not None else []

    @property
    def engines(self): return self.__engines

    def addEngine(self, engineStr):
        engineStr = engineStr.lower()
        if engineStr in self.__engines:
            log.error(f"Attempt to add a engine `{engineStr}` to user " \
                      f"category `{self.name}` that is already there. " \
                      "This should not happen.", self)
            return

        self.__engines.append(engineStr)
        self.changed.emit(self.key)

    def removeEngine(self, engineStr):
        self.__engines.remove(engineStr)
        self.changed.emit(self.key)


class CategoriesModel(QObject):  # generic
    CatModel = CategoryModel

    """ stateChanged; emitted when this category gets enabled/disabled.
    str: category key
    bool: state (enabled/disabled)
    """
    stateChanged = pyqtSignal(str, bool)
    """ changed; emitted when a category has changed (engine added or removed).
    str: category key
    """
    changed = pyqtSignal(str)
    """ removed; emitted when a category has been removed.
    str: category key
    """
    removed = pyqtSignal(str)
    """ dataChanged; emitted on setData (views must re-generate labels)
    """
    dataChanged = pyqtSignal()

    def __init__(self, parent=None):
        QObject.__init__(self, parent=parent)
        self._categories = {}

    def __contains__(self, key): return bool(key in self._categories)

    def __iter__(self): return iter(self._categories)

    def __len__(self): return len(self._categories)

    def __getitem__(self, key): return self._categories[key]

    def clear(self):
        for cat in list(self._categories.values()):
            self.removeCategory(cat.key)

    def data(self):
        return [cat.key for cat in self._categories.values()
                if cat.isChecked()]

    def setData(self, data):
        for catKey in data:
            if catKey in self:
                self[catKey].check()

        self.dataChanged.emit()

    def addCategory(self, catKey, name, checked=False):
        newCat = self.CatModel(catKey, name, checked=checked, parent=self)
        newCat.stateChanged.connect(self.stateChanged)
        newCat.changed.connect(self.changed)

        self._categories.update({catKey: newCat})
        return True

    def removeCategory(self, key):
        self._categories[key].stateChanged.disconnect(self.stateChanged)
        self._categories[key].deleteLater()
        del self._categories[key]
        self.removed.emit(key)

    def isChecked(self, key):
        return self._categories[key].isChecked()

    def checkedCategories(self):
        return [key for key in self._categories
                if self._categories[key].isChecked()]

    def items(self):
        return self._categories.items()

    def keys(self):
        return self._categories.keys()

    def values(self):
        return self._categories.values()

    def copy(self):
        return self._categories.copy()


class UserCategoriesModel(CategoriesModel):
    CatModel = UserCategoryModel

    def __init__(self, parent=None):
        CategoriesModel.__init__(self, parent=parent)

    def data(self):
        data = {}
        for catKey, cat in self._categories.items():
            data.update({catKey: (cat.name, cat.isChecked(), cat.engines)})
        return data

    def setData(self, data):
        self.clear()
        for catKey, catData in data.items():
            self.addCategory(
                catKey,
                catData[0],  # name
                checked=catData[1],
                engines=catData[2]
            )

            if catData[1]:
                self.stateChanged.emit(catKey, True)
        self.dataChanged.emit()

    def addCategory(self, catKey, name, checked=False, engines=None):
        newCat = self.CatModel(
            catKey,
            name,
            checked=checked,
            engines=engines,
            parent=self
        )

        newCat.stateChanged.connect(self.stateChanged)
        newCat.changed.connect(self.changed)

        self._categories.update({catKey: newCat})
        return True


class SearchModel(SearX, QObject):
    statusChanged = pyqtSignal(int)  # SearchStatus
    optionsChanged = pyqtSignal()

    def __init__(self, requestHandler, parent=None):
        SearX.__init__(self, requestHandler)
        QObject.__init__(self, parent=parent)

        self._status = SearchStatus.Done
        self._randomEveryRequest = False
        self._useFallback = True

    # Options
    @property
    def useFallback(self):
        """
        @rtype: bool
        """
        return self._useFallback

    @useFallback.setter
    def useFallback(self, state):
        """
        @type state: bool
        """
        self._useFallback = state
        self.optionsChanged.emit()

    @property
    def randomEvery(self):
        """
        @rtype: bool
        """
        return self._randomEveryRequest

    @randomEvery.setter
    def randomEvery(self, state):
        """
        @type state: bool
        """
        self._randomEveryRequest = state
        self.optionsChanged.emit()

    # End options

    def status(self): return self._status

    def saveSettings(self):
        """ Returns current state
        """
        return {
            'fallback': self.useFallback,
            'randomEvery': self.randomEvery,
            'parseHtml': self.parseHtml,
            'safeSearch': self.safeSearch
        }

    def loadSettings(self, data):
        """ Restore current state

        @type data: dict
        """
        self.useFallback = data.get('fallback', True)
        self.randomEvery = data.get('randomEvery', False)
        self.parseHtml = data.get('parseHtml', True)
        self.safeSearch = data.get('safeSearch', False)

    """ SearX re-implementations below
    """
    def search(self, requestKwargs={}):
        self.statusChanged.emit(SearchStatus.Busy)
        result = SearX.search(self)
        self.statusChanged.emit(SearchStatus.Done)
        return result


class UserInstancesHandler(SearxConfigHandler, ThreadManagerProto):
    """
    """
    changed = pyqtSignal()

    def __init__(self, requestsHandler, parent=None):
        """
        @param requestsHandler:
        @type requestsHandler: core.requests.RequestsHandler
        """
        SearxConfigHandler.__init__(self, requestsHandler)
        ThreadManagerProto.__init__(self, parent=parent)

        self._currentThreadUrl = ""

    # ThreadManagerProto override
    def currentJobStr(self):
        if self.hasActiveJobs():
            url = self._currentThreadUrl
            queueCount = self.queueCount()
            return _(f"<b>Updating data:</b> {url} ({queueCount} left)")
        return ""

    # HandlerProto override
    def setData(self, data):
        self._threadQueue.clear()
        SearxConfigHandler.setData(self, data)
        self.changed.emit()

    def addInstance(self, url):
        if SearxConfigHandler.addInstance(self, url):
            self.changed.emit()
            return True
        return False

    def removeMultiInstances(self, urls):
        SearxConfigHandler.removeMultiInstances(self, urls)
        self.changed.emit()

    def updateInstance(self, url):
        if self._thread:
            if url not in self._threadQueue:
                self._threadQueue.append(url)
        else:
            self._thread = Thread(
                SearxConfigHandler.updateInstance,
                args=[self, url],
                parent=self
            )

            self._currentThreadUrl = url

            self._thread.finished.connect(
                self.__updateInstanceThreadFinished
            )

            self.threadStarted.emit()
            self._thread.start()

    def __clearUpdateThread(self):
        self._thread.finished.disconnect(
            self.__updateInstanceThreadFinished
        )

        # Wait before deleting because the `finished` signal is emited
        # from the thread itself, so this method could be called before the
        # thread is actually finished and result in a crash.
        self._thread.wait()

        self._thread.deleteLater()
        self._thread = None

    def __updateInstanceThreadFinished(self):
        result = self._thread.result()

        self.__clearUpdateThread()

        if result:
            self.changed.emit()

        self._currentThreadUrl = ""

        self.threadFinished.emit()

        if self._threadQueue:
            url = self._threadQueue.pop(0)
            self.updateInstance(url)
